﻿using System;
using System.Reflection;
using System.Collections.Generic;

namespace Narwhunderful.Aphelia.Framework.IO.Serialization
{

    public class ArrayAnalyzer
    {
        private readonly object _array;
        private readonly ArrayInfo _arrayInfo;
        private IList<int[]> _indexes;

        public ArrayAnalyzer(object array)
        {
            _array = array;
            var type = array.GetType();
            _arrayInfo = GetArrayInfo(type);
        }

        public ArrayInfo ArrayInfo
        {
            get 
            { 
                return _arrayInfo; 
            }
        }

        private int GetRank(Type arrayType)
        {
            return arrayType.GetArrayRank();
        }

        private int getLength(int dimension, Type arrayType)
        {
            MethodInfo methodInfo = arrayType.GetMethod("GetLength");
            var length = (int)methodInfo.Invoke(_array, new object[] { dimension });
            return length;
        }

        private int GetLowerBound(int dimension, Type arrayType)
        {
            return GetBound("GetLowerBound", dimension, arrayType);
        }


        //        private int getUpperBound(int dimension)
        //        {
        // Not used, as UpperBound is equal LowerBound+Length
        //            return getBound("GetUpperBound", dimension);
        //        }

        private int GetBound(string methodName, int dimension, Type arrayType)
        {
            MethodInfo methodInfo = arrayType.GetMethod(methodName);
            var bound = (int)methodInfo.Invoke(_array, new object[] { dimension });
            return bound;
        }

        private ArrayInfo GetArrayInfo(Type arrayType)
        {
            // Caching is innacceptable, as an array of type string can have different bounds

            var info = new ArrayInfo();

            // Fill the dimension infos
            for (int dimension = 0; dimension < GetRank(arrayType); dimension++)
            {
                var dimensionInfo = new DimensionInfo();
                dimensionInfo.Length = getLength(dimension, arrayType);
                dimensionInfo.LowerBound = GetLowerBound(dimension, arrayType);
                info.DimensionInfos.Add(dimensionInfo);
            }


            return info;
        }

        ///<summary>
        ///</summary>
        ///<returns></returns>
        public IEnumerable<int[]> GetIndexes()
        {
            if (_indexes == null)
            {
                _indexes = new List<int[]>();
                ForEach(addIndexes);
            }

            foreach (var item in _indexes)
            {
                yield return item;
            }
        }

        ///<summary>
        ///</summary>
        ///<returns></returns>
        public IEnumerable<object> GetValues()
        {
            foreach (var indexSet in GetIndexes())
            {
                object value = ((Array)_array).GetValue(indexSet);
                yield return value;
            }
        }

        private void addIndexes(int[] obj)
        {
            _indexes.Add(obj);
        }

        public void ForEach(Action<int[]> action)
        {
            DimensionInfo dimensionInfo = _arrayInfo.DimensionInfos[0];
            for (int index = dimensionInfo.LowerBound; index < dimensionInfo.LowerBound + dimensionInfo.Length; index++)
            {
                var result = new List<int>();

                // Adding the first coordinate
                result.Add(index);

                if (_arrayInfo.DimensionInfos.Count < 2)
                {
                    // only one dimension
                    action.Invoke(result.ToArray());
                    continue;
                }

                // further dimensions
                ForEach(_arrayInfo.DimensionInfos, 1, result, action);
            }
        }

        private void ForEach(IList<DimensionInfo> dimensionInfos, int dimension, IEnumerable<int> coordinates,
                             Action<int[]> action)
        {
            DimensionInfo dimensionInfo = dimensionInfos[dimension];
            for (int index = dimensionInfo.LowerBound; index < dimensionInfo.LowerBound + dimensionInfo.Length; index++)
            {
                var result = new List<int>(coordinates);

                // Adding the first coordinate
                result.Add(index);

                if (dimension == _arrayInfo.DimensionInfos.Count - 1)
                {
                    // This is the last dimension
                    action.Invoke(result.ToArray());
                    continue;
                }

                // Further dimensions
                ForEach(_arrayInfo.DimensionInfos, dimension + 1, result, action);
            }
        }
    }

    public sealed class ArrayInfo
    {
        private IList<DimensionInfo> _dimensionInfos;

        ///<summary>
        ///</summary>
        public IList<DimensionInfo> DimensionInfos
        {
            get
            {
                if (_dimensionInfos == null) _dimensionInfos = new List<DimensionInfo>();
                return _dimensionInfos;
            }
            set { _dimensionInfos = value; }
        }
    }
}
